//------------------------------------
//Copy from C# DateTime and modified time standard.
//MaxTime is The value of this constant is equivalent to 23:59:59.9999999, December 31, 9999, exactly one 100-nanosecond tick before 00:00:00, January 1, 10000.
//------------------------------------

using System;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;

namespace Hont
{
    [Serializable]
    [StructLayout(LayoutKind.Auto)]
    public struct GameDateTime :
        IComparable,
        IFormattable,
        IConvertible,
        ISerializable,
        IComparable<GameDateTime>,
        IEquatable<GameDateTime>
    {
        const long MaxMillis = 315537897600000L;
        const long MinTicks = 0L;
        const long MaxTicks = 3155378975999999999L;
        const ulong TicksMask = 4611686018427387903uL;

        public static readonly GameDateTime MinValue = new GameDateTime(MinTicks);
        public static readonly GameDateTime MaxValue = new GameDateTime(MaxTicks);

        ulong dateData;
        long InternalTicks { get { return (long)(this.dateData & TicksMask); } }

        public int DayOfYear { get { return this.GetDatePart(1); } }
        public int Day { get { return this.GetDatePart(3); } }
        public int Month { get { return this.GetDatePart(2); } }
        public int Year { get { return this.GetDatePart(0); } }

        public int Millisecond { get { return (int)(this.InternalTicks / GameDateStandard.Instance.TicksPerMillisecond % GameDateStandard.Instance.MillisPerSecond); } }
        public int Second { get { return (int)(this.InternalTicks / GameDateStandard.Instance.TicksPerSecond % GameDateStandard.Instance.SecondPerMinute); } }
        public int Minute { get { return (int)(this.InternalTicks / GameDateStandard.Instance.TicksPerMinute % GameDateStandard.Instance.MinutePerHour); } }
        public int Hour { get { return (int)(this.InternalTicks / GameDateStandard.Instance.TicksPerHour % GameDateStandard.Instance.HourPerDay); } }
        public int DayOfWeek { get { return (int)((this.InternalTicks / GameDateStandard.Instance.TicksPerDay + 1L) % GameDateStandard.Instance.DayPerWeek); } }
        public GameTimeSpan TimeOfDay { get { return new GameTimeSpan(this.InternalTicks % GameDateStandard.Instance.TicksPerDay); } }

        public long Ticks { get { return this.InternalTicks; } }


        public GameDateTime(long ticks)
        {
            if (ticks < MinTicks || ticks > MaxTicks)
            {
                throw new ArgumentOutOfRangeException("ticks", "ArgumentOutOfRange_GameDateTimeBadTicks");
            }
            this.dateData = (ulong)ticks;
        }

        GameDateTime(ulong dateData)
        {
            this.dateData = dateData;
        }

        public GameDateTime(int year, int month, int day)
        {
            this.dateData = (ulong)DateToTicks(year, month, day);
        }

        public GameDateTime(int year, int month, int day, int hour, int minute, int second)
        {
            this.dateData = (ulong)(DateToTicks(year, month, day) + TimeToTicks(hour, minute, second));
        }

        public GameDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond)
        {
            var num = DateToTicks(year, month, day) + TimeToTicks(hour, minute, second);
            if (millisecond < 0 || millisecond >= GameDateStandard.Instance.MillisPerSecond)
            {
                throw new ArgumentOutOfRangeException("millisecond", string.Format(CultureInfo.CurrentCulture, "ArgumentOutOfRange_Range"));
            }
            num += millisecond * GameDateStandard.Instance.TicksPerMillisecond;
            if (num < MinTicks || num > MaxTicks)
            {
                throw new ArgumentException("Arg_GameDateTimeRange");
            }
            this.dateData = (ulong)num;
        }

        public static int Compare(GameDateTime t1, GameDateTime t2)
        {
            var internalTicks = t1.InternalTicks;
            var internalTicks2 = t2.InternalTicks;
            if (internalTicks > internalTicks2)
            {
                return 1;
            }
            if (internalTicks < internalTicks2)
            {
                return -1;
            }
            return 0;
        }

        public static int DaysInMonth(int month)
        {
            if (month < 1 || month > GameDateStandard.Instance.MonthPerYear)
            {
                throw new ArgumentOutOfRangeException("month", "ArgumentOutOfRange_Month");
            }

            var daysToMonth = GameDateStandard.Instance.DaysToMonth;
            return daysToMonth[month] - daysToMonth[month - 1];
        }

        public static bool Equals(GameDateTime t1, GameDateTime t2)
        {
            return t1.InternalTicks == t2.InternalTicks;
        }

        public static GameDateTime FromBinary(long dateData)
        {
            return FromBinaryRaw(dateData);
        }

        public GameDateTime Add(GameTimeSpan value)
        {
            return this.AddTicks(value.Ticks);
        }

        public GameDateTime AddDays(double value)
        {
            return this.Add(value, GameDateStandard.Instance.MillisPerDay);
        }

        public GameDateTime AddHours(double value)
        {
            return this.Add(value, GameDateStandard.Instance.MillisPerHour);
        }

        public GameDateTime AddMilliseconds(double value)
        {
            return this.Add(value, 1);
        }

        public GameDateTime AddMinutes(double value)
        {
            return this.Add(value, GameDateStandard.Instance.MillisPerMinute);
        }

        public GameDateTime AddMonths(int addMonth)
        {
            if (addMonth < -GameDateStandard.Instance.MaxMonth || addMonth > GameDateStandard.Instance.MaxMonth)
            {
                throw new ArgumentOutOfRangeException("months", "ArgumentOutOfRange_GameDateTimeBadMonths");
            }
            var num = GetDatePart(0);//year
            var num2 = GetDatePart(2);//month
            var num3 = GetDatePart(3);//day
            var num4 = num2 - 1 + addMonth;

            var monthPerYear = GameDateStandard.Instance.MonthPerYear;
            var ticksPerYear = GameDateStandard.Instance.TicksPerDay;

            if (num4 >= 0)
            {
                num2 = num4 % monthPerYear + 1;
                num += num4 / monthPerYear;
            }
            else
            {
                num2 = monthPerYear + (num4 + 1) % monthPerYear;
                num += (num4 - (monthPerYear - 1)) / monthPerYear;
            }
            if (num < 1 || num > 9999)
            {
                throw new ArgumentOutOfRangeException("months", "ArgumentOutOfRange_DateArithmetic");
            }
            int num5 = DaysInMonth(num2);
            if (num3 > num5)
            {
                num3 = num5;
            }

            return new GameDateTime((ulong)(DateToTicks(num, num2, num3) + this.InternalTicks % ticksPerYear));
        }

        public GameDateTime AddSeconds(double value)
        {
            return this.Add(value, GameDateStandard.Instance.MillisPerSecond);
        }

        public GameDateTime AddTicks(long value)
        {
            var internalTicks = this.InternalTicks;
            if (value > MaxTicks - internalTicks || value < -internalTicks)
            {
                throw new ArgumentOutOfRangeException("value", "ArgumentOutOfRange_DateArithmetic");
            }
            return new GameDateTime((ulong)(internalTicks + value));
        }

        public GameDateTime AddYears(int value)
        {
            var maxYear = GameDateStandard.Instance.MaxYear;
            if (value < -maxYear || value > maxYear)
            {
                throw new ArgumentOutOfRangeException("years", "ArgumentOutOfRange_GameDateTimeBadYears");
            }
            return this.AddMonths(value * GameDateStandard.Instance.MonthPerYear);
        }

        public GameTimeSpan Subtract(GameDateTime value)
        {
            return new GameTimeSpan(this.InternalTicks - value.InternalTicks);
        }

        public GameDateTime Subtract(GameTimeSpan value)
        {
            var internalTicks = this.InternalTicks;
            var ticks = value.Ticks;
            if (internalTicks < ticks || internalTicks - MaxTicks > ticks)
            {
                throw new ArgumentOutOfRangeException("value", "ArgumentOutOfRange_DateArithmetic");
            }
            return new GameDateTime((ulong)(internalTicks - ticks));
        }

        public long ToBinary()
        {
            return (long)this.dateData;
        }

        public int CompareTo(object value)
        {
            if (value == null)
            {
                return 1;
            }
            if (!(value is GameDateTime))
            {
                throw new ArgumentException("Arg_MustBeGameDateTime");
            }

            var internalTicks = ((GameDateTime)value).InternalTicks;
            var internalTicks2 = this.InternalTicks;
            if (internalTicks2 > internalTicks)
            {
                return 1;
            }
            if (internalTicks2 < internalTicks)
            {
                return -1;
            }
            return 0;
        }

        public int CompareTo(GameDateTime value)
        {
            long internalTicks = value.InternalTicks;
            long internalTicks2 = this.InternalTicks;
            if (internalTicks2 > internalTicks)
            {
                return 1;
            }
            if (internalTicks2 < internalTicks)
            {
                return -1;
            }
            return 0;
        }

        public override bool Equals(object value)
        {
            return value is GameDateTime && this.InternalTicks == ((GameDateTime)value).InternalTicks;
        }

        public bool Equals(GameDateTime value)
        {
            return this.InternalTicks == value.InternalTicks;
        }

        internal static GameDateTime FromBinaryRaw(long dateData)
        {
            long num = dateData & 4611686018427387903L;
            if (num < MinTicks || num > MaxTicks)
            {
                throw new ArgumentException("Argument_GameDateTimeBadBinaryData", "dateData");
            }
            return new GameDateTime((ulong)dateData);
        }

        [MethodImpl(MethodImplOptions.InternalCall)]
        internal static extern long GetSystemTimeAsFileTime();

        GameDateTime Add(double value, int scale)
        {
            var num = (long)(value * scale + ((value >= 0.0) ? 0.5 : -0.5));
            if (num <= -MaxMillis || num >= MaxMillis)
            {
                throw new ArgumentOutOfRangeException("value", "ArgumentOutOfRange_AddValue");
            }
            return this.AddTicks(num * GameDateStandard.Instance.TicksPerMillisecond);
        }

        static long DateToTicks(int year, int month, int day)
        {
            if (year >= 1 && year <= 9999 && month >= 1 && month <= GameDateStandard.Instance.MonthPerYear)
            {
                var array = GameDateStandard.Instance.DaysToMonth;
                if (day >= 1 && day <= array[month] - array[month - 1])
                {
                    long yearL = year - 1;
                    month = month - 1;
                    long dayL = day - 1;
                    var num = yearL * GameDateStandard.Instance.DayPerYear + array[month] + dayL;
                    return num * GameDateStandard.Instance.TicksPerDay;
                }
            }
            throw new ArgumentOutOfRangeException(null, string.Format("ArgumentOutOfRange_BadYearMonthDay [year:{0} month:{1} day:{2}]", year, month, day));
        }

        static long TimeToTicks(int hour, int minute, int second)
        {
            var hourPerDay = GameDateStandard.Instance.HourPerDay;
            var minutePerHour = GameDateStandard.Instance.MinutePerHour;
            var secondPerMinute = GameDateStandard.Instance.SecondPerMinute;
            var ticksPerSecond = GameDateStandard.Instance.TicksPerSecond;

            if (hour >= 0 && hour < hourPerDay && minute >= 0 && minute < minutePerHour && second >= 0 && second < secondPerMinute)
            {
                long num = hour * (secondPerMinute * minutePerHour) + minute * minutePerHour + second;

                if (num > GameTimeSpan.MaxSeconds || num < GameTimeSpan.MinSeconds)
                {
                    throw new ArgumentOutOfRangeException(null, "Overflow_TimeSpanTooLong");
                }
                return num * ticksPerSecond;
            }
            throw new ArgumentOutOfRangeException(null, "ArgumentOutOfRange_BadHourMinuteSecond");
        }

        /// <summary>
        /// part --- 0 - Year, 1 - DayOfYear, 2 - Month, 3 - Days.
        /// </summary>
        int GetDatePart(int part)
        {
            var ticksPerDay = GameDateStandard.Instance.TicksPerDay;
            var dayPerYear = GameDateStandard.Instance.DayPerYear;
            var daysToMonth = GameDateStandard.Instance.DaysToMonth;

            var days = (int)(InternalTicks / ticksPerDay);
            var year = days / dayPerYear;
            days = days + 1;

            if (part == 0)
            {
                return year + 1;
            }

            if (part == 1)
            {
                return days - year * dayPerYear;
            }

            if (part == 2)
            {
                var remainDays = (days - year * dayPerYear);

                for (int i = 1; i < daysToMonth.Length; i++)
                {
                    if (remainDays <= daysToMonth[i])
                        return i;
                }
            }

            else
            {
                var remainDays = days - year * dayPerYear;
                for (int i = 1; i < daysToMonth.Length; i++)
                {
                    if (remainDays <= daysToMonth[i])
                        return Math.Max((remainDays - daysToMonth[i - 1]), 1);
                }
            }

            return 0;
        }

        public override string ToString()
        {
            if (Hour <= 12)
                return string.Format("{0}/{1}/{2:0000} {3}:{4:00}:{5:00} {6}", Month, Day, Year, Hour, Minute, Second, "AM");
            else
                return string.Format("{0}/{1}/{2:0000} {3}:{4:00}:{5:00} {6}", Month, Day, Year, Hour - 12, Minute, Second, "PM");
        }

        public static GameDateTime operator +(GameDateTime d, GameTimeSpan t)
        {
            var internalTicks = d.InternalTicks;
            var ticks = t.Ticks;
            if (ticks > MaxTicks - internalTicks || ticks < -internalTicks)
            {
                throw new ArgumentOutOfRangeException("t", "Overflow_DateArithmetic");
            }

            return new GameDateTime(internalTicks + ticks);
        }

        public static GameDateTime operator -(GameDateTime d, GameTimeSpan t)
        {
            var internalTicks = d.InternalTicks;
            var ticks = t.Ticks;
            if (internalTicks < ticks || internalTicks - MaxTicks > ticks)
            {
                throw new ArgumentOutOfRangeException("t", "Overflow_DateArithmetic");
            }
            return new GameDateTime(internalTicks - ticks);
        }

        public static GameTimeSpan operator -(GameDateTime d1, GameDateTime d2)
        {
            return new GameTimeSpan(d1.InternalTicks - d2.InternalTicks);
        }

        public static bool operator ==(GameDateTime d1, GameDateTime d2)
        {
            return d1.InternalTicks == d2.InternalTicks;
        }

        public static bool operator !=(GameDateTime d1, GameDateTime d2)
        {
            return d1.InternalTicks != d2.InternalTicks;
        }

        public static bool operator <(GameDateTime t1, GameDateTime t2)
        {
            return t1.InternalTicks < t2.InternalTicks;
        }

        public static bool operator <=(GameDateTime t1, GameDateTime t2)
        {
            return t1.InternalTicks <= t2.InternalTicks;
        }

        public static bool operator >(GameDateTime t1, GameDateTime t2)
        {
            return t1.InternalTicks > t2.InternalTicks;
        }

        public static bool operator >=(GameDateTime t1, GameDateTime t2)
        {
            return t1.InternalTicks >= t2.InternalTicks;
        }

        public override int GetHashCode()
        {
            var internalTicks = this.InternalTicks;
            return (int)internalTicks ^ (int)(internalTicks >> 32);
        }

        public TypeCode GetTypeCode()
        {
            return GetTypeCode();
        }

        string IFormattable.ToString(string format, IFormatProvider formatProvider)
        {
            return "";
        }

        string IConvertible.ToString(IFormatProvider provider)
        {
            return "";
        }

        #region --- IConvertible Members ---

        bool IConvertible.ToBoolean(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "Boolean"
            }));
        }

        char IConvertible.ToChar(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "Char"
            }));
        }

        sbyte IConvertible.ToSByte(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "SByte"
            }));
        }

        byte IConvertible.ToByte(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "Byte"
            }));
        }

        short IConvertible.ToInt16(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "Int16"
            }));
        }

        ushort IConvertible.ToUInt16(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "UInt16"
            }));
        }

        int IConvertible.ToInt32(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "Int32"
            }));
        }

        uint IConvertible.ToUInt32(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "UInt32"
            }));
        }

        long IConvertible.ToInt64(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "Int64"
            }));
        }

        ulong IConvertible.ToUInt64(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "UInt64"
            }));
        }

        float IConvertible.ToSingle(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "Single"
            }));
        }

        double IConvertible.ToDouble(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "Double"
            }));
        }

        decimal IConvertible.ToDecimal(IFormatProvider provider)
        {
            throw new InvalidCastException(string.Format(CultureInfo.CurrentCulture, "InvalidCast_FromTo", new object[]
            {
                "GameDateTime",
                "Decimal"
            }));
        }

        DateTime IConvertible.ToDateTime(IFormatProvider provider)
        {
            return new DateTime((long)dateData);
        }

        object IConvertible.ToType(Type type, IFormatProvider provider)
        {
            return null;
        }

        #endregion

        #region --- ISerializable Members ---

        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
            {
                throw new ArgumentNullException("info");
            }
            info.AddValue("ticks", this.InternalTicks);
            info.AddValue("dateData", this.dateData);
        }

        #endregion
    }
}
